Skip to content

Instantly share code, notes, and snippets.

@Aldlevine
Last active April 6, 2022 00:02
Show Gist options
  • Save Aldlevine/93b8958a39fe7ce7ad8c2b253ccec1c5 to your computer and use it in GitHub Desktop.
Save Aldlevine/93b8958a39fe7ce7ad8c2b253ccec1c5 to your computer and use it in GitHub Desktop.
Adds an animated atlas texture to Godot
extends AtlasTexture
class_name AnimatedAtlasTexture
export(int, 1, 100) var h_frames := 1
export(int, 1, 100) var v_frames := 1
export var fps := 10.0
var previous_frame := 0
var frame := 0
func _init() -> void:
var err = VisualServer.connect("frame_pre_draw", self, "_update")
assert(err == OK)
func _update() -> void:
if atlas:
previous_frame = frame
var img := atlas.get_data()
var size = img.get_size()
var frame_size = size / Vector2(h_frames, v_frames)
frame = int(int(OS.get_ticks_msec() / (1000.0 / fps)) % (h_frames * v_frames))
var frame_pos = Vector2(frame % h_frames, floor(float(frame) / h_frames))
region = Rect2(frame_size * frame_pos, frame_size)
if previous_frame != frame:
emit_changed()
## Use this instead of a TileSet for any TileSet that contains AnimatedAtlasTexture.
## Can be added as a script to any TileSet resource, or you can create it directly in the
## TileMap's tile_set field by selecting "New AnimatedTileSet" instead of "New TileSet"
## The key is that emit_changed() must be called for the TileMap to update.
tool
extends TileSet
class_name AnimatedTileSet
var _needs_update := false
func _init() -> void:
var err
var tiles := get_tiles_ids()
for tile in tiles:
var tex = tile_get_texture(tile)
if tex is AnimatedAtlasTexture:
err = tex.connect("changed", self, "_set_needs_update", [])
assert(err == OK)
err = VisualServer.connect("frame_pre_draw", self, "_update", [], CONNECT_DEFERRED)
assert(err == OK)
func _set_needs_update() -> void:
_needs_update = true
func _update() -> void:
if _needs_update:
emit_changed()
_needs_update = false
[plugin]
name="animated_atlas_texture"
description="Adds an animated atlas texture resource"
author="Aaron Levine"
version=""
script="plugin.gd"
tool
extends EditorPlugin
var resource : AnimatedAtlasTexture
func _enter_tree():
VisualServer.connect("frame_pre_draw", self, "_update")
func _exit_tree():
VisualServer.disconnect("frame_pre_draw", self, "_update")
func handles(object: Object) -> bool:
if object is AnimatedAtlasTexture:
resource = object as AnimatedAtlasTexture
return object is AnimatedAtlasTexture
func _update() -> void:
if resource is AnimatedAtlasTexture:
if resource.atlas:
var img := resource.atlas.get_data()
var size = img.get_size()
var frame_size = size / Vector2(resource.h_frames, resource.v_frames)
var frame = int(int(OS.get_ticks_msec() / (1000.0 / resource.fps)) % (resource.h_frames * resource.v_frames))
var frame_pos = Vector2(frame % resource.h_frames, floor(float(frame) / resource.h_frames))
resource.region = Rect2(frame_size * frame_pos, frame_size)
@ek68794998
Copy link

This doesn't seem to work properly. I created a test AnimatedAtlasTexture(128x64) with h_frames = 2 as Frame 0 of an AnimatedTexture.

image

I added this to a TileMap:

image

And brushed some into a scene:

image

If I add an output during gameplay, I can see that the region is being updated:

image
image

However, there is no animation in the actual game scene. Is my setup incorrect, or is this a bug?

@Aldlevine
Copy link
Author

Aldlevine commented May 18, 2021

It looks like this code isn't working in a TIleMap. I built this for a different use case, so I never noticed. It looks like this likely has to do with some TileMap optimizations and the TileSet not knowing that the resource has changed. I've found a workaround for this that involves slight alterations to the AnimatedAtlasTexture code, as well as some additional code attached to the TileMap (could probably do this directly on the TileSet too, with alterations of course).

animated_atlas_texture.gd

The change here is adding some variables to track if the current frame is different from the previous frame. If it is different, call the emit_changed() function.

extends AtlasTexture

class_name AnimatedAtlasTexture

export(int, 1, 100) var h_frames := 1
export(int, 1, 100) var v_frames := 1
export var fps := 10.0

var previous_frame := 0
var frame := 0

func _init() -> void:
  var err = VisualServer.connect("frame_pre_draw", self, "_update")
  assert(err == OK)

func _update() -> void:
  if atlas:
    previous_frame = frame
    var img := atlas.get_data()
    var size = img.get_size()
    var frame_size = size / Vector2(h_frames, v_frames)
    frame = int(int(OS.get_ticks_msec() / (1000.0 / fps)) % (h_frames * v_frames))
    var frame_pos = Vector2(frame % h_frames, floor(float(frame) / h_frames))
    region = Rect2(frame_size * frame_pos, frame_size)
    if previous_frame != frame:
      emit_changed()

custom_tile_map.gd

When added to the TIleMap, connects to each AnimatedAtlasTexture's changed signal and calls tile_set.emit_changed(), which informs the TileMap it needs to update.

extends TileMap

var _needs_update := false

func _ready() -> void:
  var tiles := tile_set.get_tiles_ids()
  for tile in tiles:
    var tex = tile_set.tile_get_texture(tile)
    if tex is AnimatedAtlasTexture:
      tex.connect("changed", self, "_set_needs_update")

func _set_needs_update() -> void:
  _needs_update = true

func _process(_delta: float) -> void:
  if _needs_update:
    tile_set.emit_changed()
    _needs_update = false

I've updated the gist to include these changes.

@Aldlevine
Copy link
Author

Also, I didn't notice earlier, but you are using this in an AnimatedTexture, which isn't right. AnimatedAtlasTexture should replace AnimatedTexture all together (not be used as frame 0). It should animate on it's own (works out of the box with sprites and ui nodes, requires the above workaround to work with tilemaps).

@ek68794998
Copy link

Yeah, when I had it without the AnimatedTexture, it was just invisible in the game so I was using that as a workaround.

The change definitely works; I can see it animated in the TileMap editor now. One thing: I had to add a class_name in the custom TileMap script like so:

image

Otherwise it wouldn't show up in my resources list.

Thanks for looking into this and updating it!

@ek68794998
Copy link

Oh, one more thing; previous_frame is never reassigned. I added this and it still works:

image

@Aldlevine
Copy link
Author

In my setup, previous_frame is reassigned in the beginning of the update function (before frame gets rewritten), but your placement should work just the same.

Also, I've updated the solution for TileMaps by creating a custom TileSet instead. The gist was updated to include animated_tile_set.gd. I believe this is a better solution.

And one more thing, you can get better in editor previewing by turning on "Editor Settings -> Interface -> Editor -> Update Continuously", but obviously this will make the editor more resource intensive.

@boruok
Copy link

boruok commented Apr 6, 2022

Changed a lil bit, maked script straightforward + added support per AtlasTexture region (if you want make mutltiple AtlasTextures from single StreamTexture)

animated_atlas_texture.gd

class_name AnimatedAtlasTexture extends AtlasTexture


export(int, 1, 100) var h_frames := 1
export(int, 1, 100) var v_frames := 1
export var fps := 10.0

var frame_position : Vector2
var frame_size : Vector2
var frame_last := 0
var frame := 0


func init() -> void:
	frame_size = region.size / Vector2(h_frames, v_frames)
	frame_position = region.position
	VisualServer.connect("frame_pre_draw", self, "_on_frame_pre_draw")


func _on_frame_pre_draw() -> void:
	if !atlas: return

	frame = int(OS.get_ticks_msec() / (1000.0 / fps)) % (h_frames * v_frames)
	region = Rect2(
		frame_position + (frame_size * Vector2(frame % h_frames, frame / h_frames)),
		frame_size)

	if frame_last != frame:
		frame_last = frame
		emit_changed()

tilemap.gd

extends TileMap


func _ready() -> void:
	for tile in tile_set.get_tiles_ids():
		var texture := tile_set.tile_get_texture(tile)

		if texture is AnimatedAtlasTexture:
			texture.init()
			texture.connect("changed", self, "_on_AnimatedAtlasTexture_changed")


func _on_AnimatedAtlasTexture_changed() -> void:
	tile_set.emit_changed()

example regions
test

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment